/*
 *  Copyright (C) 2010 Fabien Renaud
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.boc.botv.service.impl;

import com.boc.botv.common.BetInstanceStatus;
import com.boc.botv.dao.BetChoiceDao;
import com.boc.botv.dao.BetDao;
import com.boc.botv.dao.UserDao;
import com.boc.botv.dao.BetInstanceDao;
import com.boc.botv.dao.SiteAccountDao;
import com.boc.botv.dao.TransactionDao;
import com.boc.botv.model.Bet;
import com.boc.botv.model.BetChoice;
import com.boc.botv.model.BetInstance;
import com.boc.botv.model.SiteAccount;
import com.boc.botv.model.Transaction;
import com.boc.botv.model.User;
import com.boc.botv.model.UserRememberMe;
import com.boc.botv.service.BetManager;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

/**
 * Gestionnaire de paris
 * @author Fabien Renaud
 */
@Service("betManager")
//@Transactional(readOnly = true)
public class BetManagerImpl implements BetManager {

  private static final double taxForCountry = 0.075;
  private static final double taxForSite = 0.1;
  private final Log logger = LogFactory.getLog(BetManagerImpl.class);
  private UserDao userDao;
  private BetDao betDao;
  private BetChoiceDao betChoiceDao;
  private BetInstanceDao betInstanceDao;
  private SiteAccountDao siteAccountDao;
  private TransactionDao transactionDao;

  @Autowired
  public void setDao(UserDao userDao, BetDao betDao, BetInstanceDao betInstanceDao, SiteAccountDao siteAccountDao, TransactionDao transactionDao, BetChoiceDao betChoiceDao) {
    this.userDao = userDao;
    this.betDao = betDao;
    this.betInstanceDao = betInstanceDao;
    this.siteAccountDao = siteAccountDao;
    this.transactionDao = transactionDao;
    this.betChoiceDao = betChoiceDao;
  }

  public Bet getBet(int betId) {
    logger.info("[BetManager] Getting bet by id #" + betId);
    return betDao.getBet(new Integer(betId));
  }

  public List<Bet> getBets() {
    logger.info("[BetManager] Getting bets");
    return betDao.getBets();
  }

  public List<Bet> getBetsByCreationTime(Date firstDate, Date lastDate) {
    logger.info("[BetManager] Getting bets by creation time between " + firstDate + " and " + lastDate);
    return betDao.getBetsByCreationTime(firstDate, lastDate);
  }

  public List<Bet> getBetsByStartTime(Date firstDate, Date lastDate) {
    logger.info("[BetManager] Getting bets by start time between " + firstDate + " and " + lastDate);
    return betDao.getBetsByStartTime(firstDate, lastDate);
  }

  public List<Bet> getBetsByEndTime(Date firstDate, Date lastDate) {
    logger.info("[BetManager] Getting bets by end time between " + firstDate + " and " + lastDate);
    return betDao.getBetsByEndTime(firstDate, lastDate);
  }

  public List<Bet> getBetsPlayedBetween(Date firstDate, Date lastDate) {
    logger.info("[BetManager] Getting bets with instances between : " + firstDate + " and " + lastDate);
    return betDao.getBetsPlayedBetween(firstDate, lastDate);
  }

  public List<Bet> getBetsNotPlayedBetween(Date firstDate, Date lastDate) {
    logger.info("[BetManager] Getting bets with instances between : " + firstDate + " and " + lastDate);
    return betDao.getBetsNotPlayedBetween(firstDate, lastDate);
  }

  public List<BetChoice> getChoices(int betId) {
    logger.info("[BetManager] Getting choices for bet #" + betId);
    return betChoiceDao.getChoices(betId);
  }

  public List<BetInstance> getGamesByBet(int betId) {
    logger.info("[BetManager] Getting bet instances for bet #" + betId);
    return betInstanceDao.getGamesByBet(betId);
  }

  public List<BetInstance> getGamesByChoice(int choiceId) {
    logger.info("[BetManager] Getting bet instances for bet bind with choice #" + choiceId);
    return betInstanceDao.getGamesByChoice(choiceId);
  }

  public List<User> getWinners(int betId) {
    logger.info("[BetManager] Getting winners for bet #" + betId);
    BetChoice choice = betChoiceDao.getWinningChoice(betId);
    if (choice != null) {
      return this.getUserWhoChoosed(choice.getId());
    }
    return null;
  }

  public List<User> getLosers(int betId) {
    logger.info("[BetManager] Getting losers for bet #" + betId);
    List<BetChoice> choices = betChoiceDao.getLosingChoices(betId);
    if (choices == null) {
      return null;
    }
    List<User> users = new ArrayList<User>();
    for (BetChoice bc : choices) {
      List<User> ul = this.getUserWhoChoosed(bc.getId());
      if (ul != null) {
        users.addAll(ul);
      }
    }
    return users;
  }

  public List<User> getUserWhoChoosed(int choiceId) {
    logger.info("[BetManager] Getting users who choosed choice : " + choiceId);
    List<BetInstance> bil = betInstanceDao.getGamesByChoice(choiceId);
    List<User> users = new ArrayList<User>();
    for (BetInstance bi : bil) {
      users.add(bi.getUser());
    }
    return users;
  }

  //@Transactional(readOnly = false)
  public void createBet(Bet bet) {
    logger.info("[BetManager] Creating bet #" + bet.getId());
    betDao.saveBet(bet);
  }

  //@Transactional(readOnly = false)
  public void updateBet(Bet bet) {
    logger.info("[BetManager] Updating bet #" + bet.getId());
    betDao.updateBet(bet);
  }

  //@Transactional(readOnly = false)
  public void deleteBet(Bet bet) {
    logger.info("[BetManager] Deleting bet #" + bet.getId());
    betDao.deleteBet(bet);
  }

  //@Transactional(readOnly = false)
  public boolean changeName(int betId, String name) {
    if (betId < 1) {
      return false;
    }
    logger.info("[BetManager] Changing name " + name + " for bet " + betId);

    Bet bet = betDao.getBet(betId);
    bet.setName(name);
    return true;
  }

  //@Transactional(readOnly = false)
  public boolean changeDescription(int betId, String description) {
    if (betId < 1) {
      return false;
    }
    logger.info("[BetManager] Changing description " + description + " for bet " + betId);

    Bet bet = betDao.getBet(betId);
    bet.setDescription(description);
    return true;
  }

  //@Transactional(readOnly = false)
  public boolean addChoice(Bet bet, String name) {
    if (bet == null || name == null || name.length() < 1) {
      return false;
    }
    logger.info("[BetManager] Adding choice `" + name + "` for bet " + bet.getId());

    BetChoice choice = new BetChoice(bet, name, false);
    betChoiceDao.saveChoice(choice);
    return true;
  }

  //@Transactional(readOnly = false)
  public boolean changeChoice(int choiceId, String label) {
    if (choiceId < 1 || label == null || label.length() < 1) {
      return false;
    }
    logger.info("[BetManager] Changing choice `" + choiceId + "'");

    BetChoice choice = betChoiceDao.getChoice(choiceId);
    choice.setLabel(label);
    return true;
  }

  //@Transactional(readOnly = false)
  public boolean deleteChoice(int choiceId) {
    if (choiceId < 1) {
      return false;
    }
    logger.info("[BetManager] Deleting choice `" + choiceId + "'");

    BetChoice bc = betChoiceDao.getChoice(choiceId);
    if (bc != null) {
      return false;
    }
    betChoiceDao.deleteChoice(bc);
    return true;
  }

  //@Transactional(readOnly = false)
  public boolean addGamer(int userId, int choiceId, double amount) {
    if (userId < 1 || choiceId < 1 || amount < 1) {
      return false;
    }
    logger.info("[BetManager] Adding choice " + choiceId + " with an amount of " + amount + " for user " + userId);

    User user = userDao.getUser(userId);
    if (user == null || user.getBalance() < amount) {
      return false;
    }
    BetChoice bc = betChoiceDao.getChoice(choiceId);
    if (bc == null) {
      return false;
    }
    Date now = new Date();
    Bet b = bc.getBet();
    if (now.before(b.getStartTime()) || now.after(b.getEndTime())) {
      return false;
    }

    String label = String.format("PRÉLÈVEMENT PARI\n%1$s | CHOIX : `%2$s`", b.getName(), bc.getLabel());
    SiteAccount sa = siteAccountDao.getAccount(1);

    logger.info("[BetManager] Crediting (output) client account");
    user.decreaseBalance(amount);
    Authentication a = SecurityContextHolder.getContext().getAuthentication();
    ((UserRememberMe) a.getPrincipal()).setBalance(user.getBalance());

    logger.info("[BetManager] Establishing transaction");
    Transaction t = new Transaction(user, sa, label, now, amount, 0);
    transactionDao.saveTransaction(t);

    logger.info("[BetManager] Creating the bet instance");
    BetInstance bi = new BetInstance(bc, user, b, amount, now, BetInstanceStatus.playing);
    betInstanceDao.saveGame(bi);
    return true;
  }

  //@Transactional(readOnly = false)
  public boolean setWinningChoice(int choiceId) {
    if (choiceId < 1) {
      return false;
    }
    logger.info("[BetManager] Setting bet choice #" + choiceId + " to the `win` status.");

    BetChoice winningChoice = betChoiceDao.getChoice(choiceId);
    if (winningChoice == null) {
      return false;
    }
    winningChoice.setIsWinning(true);
    //betChoiceDao.updateChoice(winningChoice);

    logger.info("[BetManager] Updating bet parameters...");
    logger.info("[BetManager] Calculating amounts.");
    double totalAmount = 0;
    double totalWinnersAmount = 0;
    int betId = winningChoice.getBet().getId();
    List<BetInstance> games = this.getGamesByBet(betId);
    List<BetInstance> winningGames = new ArrayList<BetInstance>();
    for (BetInstance g : games) {
      totalAmount += g.getBetAmount();
      if (g.getBetChoice().getId() == choiceId) {
        totalWinnersAmount += g.getBetAmount();
        winningGames.add(g);
      }
    }

    // aucun parieur n'a gagné
    if (totalWinnersAmount == 0) {
      // montant taxe Etat déduite
      double stateTax = 0;
      double amountForSite = 0;
      SiteAccount sa1 = siteAccountDao.getAccount(1); // Compte Paris
      SiteAccount sa2 = siteAccountDao.getAccount(2); // Compte Taxe Site
      SiteAccount sa3 = siteAccountDao.getAccount(3); // Compte Taxe État
      User admin = userDao.getUser("admin");
      String betName = winningChoice.getBet().getName();
      String choiceName = winningChoice.getLabel();
      String label1 = String.format("GAIN DU SITE POUR LE PARI\n`%1$s` | CHOIX `%2$s`", betName, choiceName);
      String label2 = "PRÉLÈVEMENT TAXE POUR LE SITE";
      String label3 = "PRÉLÈVEMENT TAXE POUR L'ÉTAT";
      Date now = new Date();

      // on crédite le site de l'admin
      Transaction t = new Transaction(admin, sa1, label1, now, 0, totalAmount);
      transactionDao.saveTransaction(t);
      admin.increaseBalance(amountForSite);

      // on prélève les taxes pour le site
      amountForSite = totalAmount * (1 - taxForCountry);
      t = new Transaction(admin, sa2, label2, now, amountForSite, 0);
      transactionDao.saveTransaction(t);
      admin.decreaseBalance(amountForSite);

      // on prélève les taxes pour l'état
      stateTax = totalAmount * taxForCountry;
      t = new Transaction(admin, sa3, label3, now, stateTax, 0);
      transactionDao.saveTransaction(t);
      admin.decreaseBalance(stateTax);
      return true;
    }

    double totalRemainder = 0;
    boolean doTaxSite = true;
    boolean doTaxCountry = true;
    if ((totalRemainder = totalAmount * (1 - (taxForSite + taxForCountry))) < totalWinnersAmount) {
      if ((totalRemainder = totalAmount * (1 - (taxForCountry))) < totalWinnersAmount) {
        doTaxCountry = false;
        if ((totalRemainder = totalAmount * (1 - (taxForSite))) < totalWinnersAmount) {
          doTaxSite = false;
          totalRemainder = totalAmount;
        }
      }
    }

    logger.info("[BetManager] Calculating and applying winnings");
    double baseratio = totalAmount / totalWinnersAmount;
    double ratio = totalRemainder / totalWinnersAmount;
    double amount = 0;
    Date now = new Date();
    String betName = winningChoice.getBet().getName();
    String choiceName = winningChoice.getLabel();
    String label1 = String.format("GAIN PARI\n`%1$s` | CHOIX `%2$s`", betName, choiceName);
    String label2 = "PRÉLÈVEMENT TAXE POUR LE SITE";
    String label3 = "PRÉLÈVEMENT TAXE POUR L'ÉTAT";
    SiteAccount sa1 = siteAccountDao.getAccount(1); // Compte Paris
    SiteAccount sa2 = siteAccountDao.getAccount(2); // Compte Taxe Site
    SiteAccount sa3 = siteAccountDao.getAccount(3); // Compte Taxe État
    for (BetInstance w : winningGames) {
      User u = w.getUser();
      // Du compte des paris vers le compte utilisateur : tous les gains non taxés.
      amount = baseratio * w.getBetAmount();
      Transaction t = new Transaction(w.getUser(), sa1, label1, now, 0, amount);
      transactionDao.saveTransaction(t);
      u.increaseBalance(amount);

      // Prélèvement des taxes.
      if (doTaxSite) {
        amount = w.getBetAmount() * taxForSite;
        t = new Transaction(w.getUser(), sa1, label2, now, amount, 0);
        transactionDao.saveTransaction(t);
        u.decreaseBalance(amount);
      }

      if (doTaxCountry) {
        amount = w.getBetAmount() * taxForCountry;
        t = new Transaction(w.getUser(), sa1, label3, now, amount, 0);
        transactionDao.saveTransaction(t);
        u.decreaseBalance(amount);
      }
    }

    return true;
  }

  public List<Bet> getBetBySubsection(int subsection) {
    logger.info("[BetManager] Getting bets by subsection");
    return betDao.getBetsBySubsection(subsection);
  }

  public List<Bet> getBetsByCloserEndDate(int numberOfBet, int subsection) {
    logger.info("[BetManager] Getting bets by subsection and closest end time");
    return betDao.getBetsByCloserEndDate(numberOfBet, subsection);
  }

  public boolean saveOrUpdate(User admin, int id, String name, String desc, Date creationDate, Date startDate, Date endDate, int section, boolean actif) {
    // boolean changed = false;
    // création d'un nouveau pari
    if (id == -1) {
      Bet bet = new Bet(admin, name, desc, creationDate, startDate, endDate, section, actif);
      betDao.saveBet(bet);
    } else {
      Bet bet = betDao.getBet(id);
      if (!bet.getName().equals(name)) {
        bet.setName(name);
      }
      if (!bet.getDescription().equals(desc)) {
        bet.setDescription(desc);
      }
      if (!bet.getStartTime().equals(creationDate)) {
        bet.setStartTime(startDate);
      }
      if (!bet.getEndTime().equals(endDate)) {
        bet.setEndTime(endDate);
      }
      if (!(bet.getSubsection() == section)) {
        bet.setSubsection(section);
      }
      if (!(bet.isStatus() == actif)) {
        bet.setStatus(actif);
      }

      betDao.updateBet(bet);
    }
    return true;
  }

  public List<Bet> getCurrentBetsBySubsection(int subsection) {
    logger.info("[BetManager] Getting playable bets by subsection");
    return betDao.getCurrentBetsBySubsection(subsection);
  }
}
